"""
文件名: search_service.py
描述: 智能搜索和筛选服务

本模块包含以下主要功能：
1. 多维度搜索功能（视频、频道、标签）
2. 高级筛选条件组合
3. 搜索结果排序和分页
4. 搜索历史和热门搜索
5. 智能推荐和相关搜索

依赖模块:
   - sqlalchemy: 数据库操作
   - datetime: 时间处理
   - typing: 类型注解
   - re: 正则表达式

使用示例:
   >>> search_service = SearchService()
   >>> results = await search_service.search_videos(query, user_id, filters)
   >>> suggestions = await search_service.get_search_suggestions(query, user_id)
   >>> history = await search_service.get_search_history(user_id)

注意事项:
   - 搜索结果要去重和排序
   - 支持模糊匹配和精确匹配
   - 缓存热门搜索结果
   - 搜索历史要限制数量
   - 要处理特殊字符和注入

作者: AI助手
创建日期: 2024-06-27
最后修改: 2024-06-27
版本: 1.0.0
"""

import re
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any, Set
import uuid

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_, or_, text, desc, asc
from sqlalchemy.orm import selectinload, joinedload

from src.core.database import get_db_session
from src.core.redis import redis_manager
from src.core.logger import get_logger
from src.models.video import Video, VideoSummary, VideoFilter
from src.models.channel import Channel, ChannelSummary
from src.models.category import Category, channel_category_association
from src.services.video_service import VideoService
from src.utils.exceptions import (
    DatabaseException,
    ValidationException,
)

logger = get_logger(__name__)


class SearchService:
    """搜索和筛选服务类"""
    
    def __init__(self):
        """初始化搜索服务"""
        self.redis = redis_manager
        self.video_service = VideoService()
        
        # 缓存配置
        self.search_cache_ttl = 300  # 搜索结果缓存5分钟
        self.suggestion_cache_ttl = 1800  # 搜索建议缓存30分钟
        self.history_cache_ttl = 86400  # 搜索历史缓存1天
        
        # 搜索配置
        self.max_search_history = 50  # 最大搜索历史数量
        self.min_query_length = 2  # 最小搜索词长度
        self.max_suggestions = 10  # 最大搜索建议数量
    
    async def search_videos(
        self,
        query: str,
        user_id: uuid.UUID,
        filters: Optional[VideoFilter] = None,
        limit: int = 20,
        offset: int = 0,
    ) -> Dict[str, Any]:
        """
        搜索视频内容
        
        Args:
            query: 搜索关键词
            user_id: 用户ID
            filters: 筛选条件
            limit: 每页数量
            offset: 偏移量
            
        Returns:
            Dict[str, Any]: 搜索结果
        """
        try:
            # 验证搜索词
            cleaned_query = self._clean_search_query(query)
            if not cleaned_query or len(cleaned_query) < self.min_query_length:
                return {
                    "videos": [],
                    "total": 0,
                    "query": cleaned_query,
                    "suggestions": [],
                }
            
            # 检查缓存
            cache_key = self._generate_search_cache_key(
                "videos", cleaned_query, user_id, filters, limit, offset
            )
            cached_result = await self.redis.get(cache_key)
            if cached_result:
                logger.debug(f"从缓存获取搜索结果", query=cleaned_query)
                return cached_result
            
            # 记录搜索历史
            await self._record_search_history(user_id, cleaned_query, "video")
            
            async with get_db_session() as db:
                # 构建搜索查询
                search_query = self._build_video_search_query(db, cleaned_query, user_id, filters)
                
                # 获取视频列表
                videos_query = search_query.limit(limit).offset(offset)
                videos_result = await db.execute(videos_query)
                videos = videos_result.scalars().all()
                
                # 获取总数
                count_query = select(func.count()).select_from(search_query.subquery())
                count_result = await db.execute(count_query)
                total_count = count_result.scalar()
                
                # 转换为摘要格式
                video_summaries = []
                for video in videos:
                    summary = VideoSummary(
                        id=video.id,
                        video_id=video.video_id,
                        title=video.title,
                        thumbnail_url=video.thumbnail_url,
                        duration_seconds=video.duration_seconds,
                        view_count=video.view_count,
                        published_at=video.published_at,
                        is_live=video.is_live,
                        is_short=video.is_short,
                    )
                    video_summaries.append(summary)
                
                # 获取搜索建议
                suggestions = await self._get_related_suggestions(cleaned_query, user_id)
                
                result = {
                    "videos": video_summaries,
                    "total": total_count,
                    "query": cleaned_query,
                    "suggestions": suggestions,
                    "filters_applied": filters.dict() if filters else {},
                }
                
                # 缓存结果
                await self.redis.set(cache_key, result, expire=self.search_cache_ttl)
                
                logger.info(f"视频搜索完成", query=cleaned_query, total=total_count)
                return result
                
        except Exception as e:
            logger.error(f"搜索视频失败: {e}", query=query, user_id=str(user_id))
            raise DatabaseException(f"搜索失败: {str(e)}")
    
    async def search_channels(
        self,
        query: str,
        user_id: uuid.UUID,
        limit: int = 20,
        offset: int = 0,
    ) -> Dict[str, Any]:
        """
        搜索频道
        
        Args:
            query: 搜索关键词
            user_id: 用户ID
            limit: 每页数量
            offset: 偏移量
            
        Returns:
            Dict[str, Any]: 搜索结果
        """
        try:
            # 验证搜索词
            cleaned_query = self._clean_search_query(query)
            if not cleaned_query or len(cleaned_query) < self.min_query_length:
                return {
                    "channels": [],
                    "total": 0,
                    "query": cleaned_query,
                }
            
            # 检查缓存
            cache_key = self._generate_search_cache_key(
                "channels", cleaned_query, user_id, None, limit, offset
            )
            cached_result = await self.redis.get(cache_key)
            if cached_result:
                return cached_result
            
            # 记录搜索历史
            await self._record_search_history(user_id, cleaned_query, "channel")
            
            async with get_db_session() as db:
                # 构建频道搜索查询
                search_term = f"%{cleaned_query}%"
                
                search_query = (
                    select(Channel)
                    .join(channel_category_association)
                    .join(Category)
                    .where(
                        and_(
                            Category.user_id == user_id,
                            Category.deleted_at.is_(None),
                            Channel.deleted_at.is_(None),
                            or_(
                                Channel.title.ilike(search_term),
                                Channel.description.ilike(search_term),
                                Channel.custom_url.ilike(search_term)
                            )
                        )
                    )
                    .distinct()
                    .order_by(
                        # 标题完全匹配优先
                        Channel.title.ilike(f"{cleaned_query}").desc(),
                        # 订阅者数量降序
                        Channel.subscriber_count.desc(),
                        # 标题排序
                        Channel.title
                    )
                )
                
                # 获取频道列表
                channels_query = search_query.limit(limit).offset(offset)
                channels_result = await db.execute(channels_query)
                channels = channels_result.scalars().all()
                
                # 获取总数
                count_query = select(func.count(func.distinct(Channel.id))).select_from(
                    search_query.subquery()
                )
                count_result = await db.execute(count_query)
                total_count = count_result.scalar()
                
                # 转换为摘要格式
                channel_summaries = []
                for channel in channels:
                    summary = ChannelSummary(
                        id=channel.id,
                        channel_id=channel.channel_id,
                        title=channel.title,
                        thumbnail_url=channel.thumbnail_url,
                        subscriber_count=channel.subscriber_count,
                        video_count=channel.video_count,
                        last_video_published_at=channel.last_video_published_at,
                        sync_status=channel.sync_status,
                    )
                    channel_summaries.append(summary)
                
                result = {
                    "channels": channel_summaries,
                    "total": total_count,
                    "query": cleaned_query,
                }
                
                # 缓存结果
                await self.redis.set(cache_key, result, expire=self.search_cache_ttl)
                
                logger.info(f"频道搜索完成", query=cleaned_query, total=total_count)
                return result
                
        except Exception as e:
            logger.error(f"搜索频道失败: {e}", query=query, user_id=str(user_id))
            raise DatabaseException(f"搜索失败: {str(e)}")
    
    async def get_search_suggestions(
        self,
        query: str,
        user_id: uuid.UUID,
        limit: int = 10,
    ) -> List[str]:
        """
        获取搜索建议
        
        Args:
            query: 搜索关键词
            user_id: 用户ID
            limit: 建议数量
            
        Returns:
            List[str]: 搜索建议列表
        """
        try:
            # 验证搜索词
            cleaned_query = self._clean_search_query(query)
            if not cleaned_query:
                return []
            
            # 检查缓存
            cache_key = f"search_suggestions:{user_id}:{cleaned_query}:{limit}"
            cached_suggestions = await self.redis.get(cache_key)
            if cached_suggestions:
                return cached_suggestions
            
            suggestions = set()
            
            async with get_db_session() as db:
                # 从视频标题获取建议
                video_suggestions = await self._get_video_title_suggestions(
                    db, cleaned_query, user_id, limit // 2
                )
                suggestions.update(video_suggestions)
                
                # 从频道名称获取建议
                channel_suggestions = await self._get_channel_name_suggestions(
                    db, cleaned_query, user_id, limit // 2
                )
                suggestions.update(channel_suggestions)
                
                # 从标签获取建议
                tag_suggestions = await self._get_tag_suggestions(
                    db, cleaned_query, user_id, limit // 3
                )
                suggestions.update(tag_suggestions)
            
            # 从搜索历史获取建议
            history_suggestions = await self._get_history_suggestions(
                user_id, cleaned_query, limit // 3
            )
            suggestions.update(history_suggestions)
            
            # 转换为列表并排序
            suggestion_list = list(suggestions)[:limit]
            suggestion_list.sort(key=lambda x: (
                # 完全匹配优先
                not x.lower().startswith(cleaned_query.lower()),
                # 长度较短优先
                len(x),
                # 字母顺序
                x.lower()
            ))
            
            # 缓存结果
            await self.redis.set(cache_key, suggestion_list, expire=self.suggestion_cache_ttl)
            
            return suggestion_list
            
        except Exception as e:
            logger.error(f"获取搜索建议失败: {e}", query=query)
            return []
    
    async def get_search_history(
        self,
        user_id: uuid.UUID,
        limit: int = 20,
    ) -> List[Dict[str, Any]]:
        """
        获取用户搜索历史
        
        Args:
            user_id: 用户ID
            limit: 历史数量
            
        Returns:
            List[Dict[str, Any]]: 搜索历史列表
        """
        try:
            cache_key = f"search_history:{user_id}"
            history = await self.redis.get(cache_key)
            
            if not history:
                return []
            
            # 按时间倒序排序并限制数量
            sorted_history = sorted(
                history, 
                key=lambda x: x.get("timestamp", 0), 
                reverse=True
            )[:limit]
            
            return sorted_history
            
        except Exception as e:
            logger.error(f"获取搜索历史失败: {e}", user_id=str(user_id))
            return []
    
    async def clear_search_history(self, user_id: uuid.UUID) -> bool:
        """
        清空用户搜索历史
        
        Args:
            user_id: 用户ID
            
        Returns:
            bool: 是否清空成功
        """
        try:
            cache_key = f"search_history:{user_id}"
            await self.redis.delete(cache_key)
            
            logger.info(f"用户搜索历史已清空", user_id=str(user_id))
            return True
            
        except Exception as e:
            logger.error(f"清空搜索历史失败: {e}", user_id=str(user_id))
            return False
    
    async def get_popular_searches(
        self,
        user_id: Optional[uuid.UUID] = None,
        limit: int = 10,
    ) -> List[Dict[str, Any]]:
        """
        获取热门搜索
        
        Args:
            user_id: 用户ID（可选，用于个性化）
            limit: 热门搜索数量
            
        Returns:
            List[Dict[str, Any]]: 热门搜索列表
        """
        try:
            # 全局热门搜索
            global_cache_key = "popular_searches:global"
            global_popular = await self.redis.get(global_cache_key)
            
            if not global_popular:
                # 如果没有缓存，返回默认热门搜索
                global_popular = [
                    {"query": "科技", "count": 100},
                    {"query": "游戏", "count": 80},
                    {"query": "音乐", "count": 75},
                    {"query": "教程", "count": 70},
                    {"query": "新闻", "count": 65},
                ]
            
            result = global_popular[:limit]
            
            # 如果提供了用户ID，添加个性化推荐
            if user_id:
                personal_suggestions = await self._get_personalized_searches(user_id, limit // 2)
                result.extend(personal_suggestions)
                
                # 去重并限制数量
                seen = set()
                unique_result = []
                for item in result:
                    if item["query"] not in seen:
                        seen.add(item["query"])
                        unique_result.append(item)
                
                result = unique_result[:limit]
            
            return result
            
        except Exception as e:
            logger.error(f"获取热门搜索失败: {e}")
            return []
    
    def _clean_search_query(self, query: str) -> str:
        """
        清理搜索查询词
        
        Args:
            query: 原始查询词
            
        Returns:
            str: 清理后的查询词
        """
        if not query:
            return ""
        
        # 移除多余空格
        cleaned = re.sub(r'\s+', ' ', query.strip())
        
        # 移除特殊字符（保留中文、英文、数字、空格）
        cleaned = re.sub(r'[^\w\s\u4e00-\u9fff]', '', cleaned)
        
        return cleaned
    
    def _generate_search_cache_key(
        self,
        search_type: str,
        query: str,
        user_id: uuid.UUID,
        filters: Optional[VideoFilter],
        limit: int,
        offset: int,
    ) -> str:
        """生成搜索缓存键"""
        filters_str = ""
        if filters:
            filters_str = str(hash(str(filters.dict())))
        
        return f"search:{search_type}:{user_id}:{hash(query)}:{filters_str}:{limit}:{offset}"
    
    def _build_video_search_query(
        self,
        db: AsyncSession,
        query: str,
        user_id: uuid.UUID,
        filters: Optional[VideoFilter],
    ):
        """构建视频搜索查询"""
        search_term = f"%{query}%"
        
        # 基础查询
        base_query = (
            select(Video)
            .join(Channel, Video.channel_id == Channel.id)
            .join(channel_category_association, Channel.id == channel_category_association.c.channel_id)
            .join(Category, channel_category_association.c.category_id == Category.id)
            .where(
                and_(
                    Category.user_id == user_id,
                    Category.deleted_at.is_(None),
                    Channel.deleted_at.is_(None),
                    or_(
                        Video.cache_expires_at.is_(None),
                        Video.cache_expires_at > datetime.utcnow()
                    ),
                    # 搜索条件
                    or_(
                        Video.title.ilike(search_term),
                        Video.description.ilike(search_term),
                        Channel.title.ilike(search_term),
                        # 搜索标签
                        func.exists().where(
                            func.jsonb_array_elements_text(Video.tags).ilike(search_term)
                        )
                    )
                )
            )
            .distinct()
        )
        
        # 应用筛选条件
        if filters:
            base_query = self.video_service._apply_video_filters(base_query, filters)
        
        # 默认排序：相关性 + 发布时间
        base_query = base_query.order_by(
            # 标题完全匹配优先
            Video.title.ilike(f"{query}").desc(),
            # 最新发布优先
            Video.published_at.desc(),
            # 观看量降序
            Video.view_count.desc()
        )
        
        return base_query
    
    async def _get_video_title_suggestions(
        self,
        db: AsyncSession,
        query: str,
        user_id: uuid.UUID,
        limit: int,
    ) -> List[str]:
        """从视频标题获取搜索建议"""
        try:
            search_term = f"%{query}%"
            
            stmt = (
                select(Video.title)
                .join(Channel, Video.channel_id == Channel.id)
                .join(channel_category_association, Channel.id == channel_category_association.c.channel_id)
                .join(Category, channel_category_association.c.category_id == Category.id)
                .where(
                    and_(
                        Category.user_id == user_id,
                        Category.deleted_at.is_(None),
                        Video.title.ilike(search_term),
                        Video.title != ""
                    )
                )
                .distinct()
                .limit(limit)
            )
            
            result = await db.execute(stmt)
            titles = result.scalars().all()
            
            # 提取关键词
            suggestions = []
            for title in titles:
                words = self._extract_keywords(title, query)
                suggestions.extend(words)
            
            return list(set(suggestions))[:limit]
            
        except Exception as e:
            logger.error(f"获取视频标题建议失败: {e}")
            return []
    
    async def _get_channel_name_suggestions(
        self,
        db: AsyncSession,
        query: str,
        user_id: uuid.UUID,
        limit: int,
    ) -> List[str]:
        """从频道名称获取搜索建议"""
        try:
            search_term = f"%{query}%"
            
            stmt = (
                select(Channel.title)
                .join(channel_category_association)
                .join(Category)
                .where(
                    and_(
                        Category.user_id == user_id,
                        Category.deleted_at.is_(None),
                        Channel.title.ilike(search_term),
                        Channel.title != ""
                    )
                )
                .distinct()
                .limit(limit)
            )
            
            result = await db.execute(stmt)
            names = result.scalars().all()
            
            return [name for name in names if query.lower() in name.lower()][:limit]
            
        except Exception as e:
            logger.error(f"获取频道名称建议失败: {e}")
            return []
    
    async def _get_tag_suggestions(
        self,
        db: AsyncSession,
        query: str,
        user_id: uuid.UUID,
        limit: int,
    ) -> List[str]:
        """从视频标签获取搜索建议"""
        try:
            # 使用PostgreSQL的JSONB功能搜索标签
            stmt = text("""
                SELECT DISTINCT tag
                FROM (
                    SELECT jsonb_array_elements_text(v.tags) as tag
                    FROM videos v
                    JOIN channels c ON v.channel_id = c.id
                    JOIN channel_categories cc ON c.id = cc.channel_id
                    JOIN categories cat ON cc.category_id = cat.id
                    WHERE cat.user_id = :user_id
                      AND cat.deleted_at IS NULL
                      AND v.tags IS NOT NULL
                ) t
                WHERE LOWER(tag) LIKE LOWER(:search_term)
                  AND tag != ''
                LIMIT :limit
            """)
            
            result = await db.execute(stmt, {
                "user_id": str(user_id),
                "search_term": f"%{query}%",
                "limit": limit
            })
            
            tags = [row[0] for row in result]
            return tags
            
        except Exception as e:
            logger.error(f"获取标签建议失败: {e}")
            return []
    
    async def _get_history_suggestions(
        self,
        user_id: uuid.UUID,
        query: str,
        limit: int,
    ) -> List[str]:
        """从搜索历史获取建议"""
        try:
            history = await self.get_search_history(user_id, self.max_search_history)
            
            suggestions = []
            for item in history:
                search_query = item.get("query", "")
                if (search_query and 
                    query.lower() in search_query.lower() and 
                    search_query != query):
                    suggestions.append(search_query)
            
            return suggestions[:limit]
            
        except Exception as e:
            logger.error(f"获取历史搜索建议失败: {e}")
            return []
    
    def _extract_keywords(self, text: str, query: str) -> List[str]:
        """从文本中提取关键词"""
        if not text:
            return []
        
        # 简单的关键词提取逻辑
        words = re.findall(r'\b\w+\b', text.lower())
        
        # 过滤掉太短的词和常见停用词
        stop_words = {"的", "是", "在", "了", "和", "有", "中", "to", "the", "a", "an", "and", "or"}
        keywords = [
            word for word in words 
            if len(word) >= 2 and word not in stop_words and query.lower() in word
        ]
        
        return keywords[:5]  # 最多返回5个关键词
    
    async def _record_search_history(
        self,
        user_id: uuid.UUID,
        query: str,
        search_type: str,
    ) -> None:
        """记录搜索历史"""
        try:
            cache_key = f"search_history:{user_id}"
            history = await self.redis.get(cache_key) or []
            
            # 新的搜索记录
            new_record = {
                "query": query,
                "type": search_type,
                "timestamp": datetime.utcnow().timestamp(),
            }
            
            # 移除重复的搜索
            history = [item for item in history if item.get("query") != query]
            
            # 添加新记录到开头
            history.insert(0, new_record)
            
            # 限制历史数量
            history = history[:self.max_search_history]
            
            # 保存到缓存
            await self.redis.set(cache_key, history, expire=self.history_cache_ttl)
            
        except Exception as e:
            logger.error(f"记录搜索历史失败: {e}")
    
    async def _get_related_suggestions(
        self,
        query: str,
        user_id: uuid.UUID,
        limit: int = 5,
    ) -> List[str]:
        """获取相关搜索建议"""
        try:
            # 简单的相关搜索逻辑
            suggestions = []
            
            # 添加查询词的变体
            if len(query) > 3:
                suggestions.extend([
                    f"{query} 教程",
                    f"{query} 最新",
                    f"{query} 2024",
                    f"如何 {query}",
                    f"{query} 评测",
                ])
            
            return suggestions[:limit]
            
        except Exception as e:
            logger.error(f"获取相关建议失败: {e}")
            return []
    
    async def _get_personalized_searches(
        self,
        user_id: uuid.UUID,
        limit: int,
    ) -> List[Dict[str, Any]]:
        """获取个性化搜索推荐"""
        try:
            # 基于用户搜索历史的个性化推荐
            history = await self.get_search_history(user_id, 20)
            
            # 统计搜索词频率
            query_counts = {}
            for item in history:
                query = item.get("query", "")
                if query:
                    query_counts[query] = query_counts.get(query, 0) + 1
            
            # 转换为推荐格式
            personal_searches = []
            for query, count in sorted(query_counts.items(), key=lambda x: x[1], reverse=True):
                personal_searches.append({
                    "query": query,
                    "count": count,
                    "personal": True,
                })
            
            return personal_searches[:limit]
            
        except Exception as e:
            logger.error(f"获取个性化搜索失败: {e}")
            return []


# 导出主要接口
__all__ = [
    "SearchService",
]